﻿#region

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Threading;
using Amib.Threading;
using MediaSage.Lib.Configuration;
using MediaSage.Lib.Database;
using MediaSage.Lib.Plugin;
using MediaSage.Lib.Utilities;

#endregion

namespace MediaSage.Lib.ThreadManager
{
    /// <summary>
    ///     Class for handling threads within MediaSage
    ///     There are 3 types of threads:
    ///     1. ImageUpdate - Retreiving threads via a URL
    ///     2. Updating a media item
    ///     3. Running a maintenance task
    /// </summary>
    public class ThreadManager
    {
        #region Fields

        [ThreadStatic]
        private readonly Config config;

        [ThreadStatic]
        private readonly Logger logger;

        private readonly SmartThreadPool threadPool;

        [ThreadStatic]
        private readonly IDictionary<Guid, IWorkItemResult<object>> threadResults;

        #endregion Fields

        #region Constructors

        public ThreadManager(Config Configuration)
        {
            config = Configuration;
            logger = Configuration.Logger;
            var stpStartInfo = new STPStartInfo();
            stpStartInfo.MaxWorkerThreads = config.MaxWorkerThreads * 2;
            stpStartInfo.MinWorkerThreads = config.MaxWorkerThreads;
            stpStartInfo.ThreadPriority = ThreadPriority.BelowNormal;
            stpStartInfo.DisposeOfStateObjects = true;
            stpStartInfo.ApartmentState = ApartmentState.MTA;
            stpStartInfo.AreThreadsBackground = true;
            stpStartInfo.IdleTimeout = 500;

            threadPool = new SmartThreadPool(stpStartInfo);
            threadResults = new Dictionary<Guid, IWorkItemResult<object>>();

            logger.Log("ThreadManager Initialized");
        }

        ~ThreadManager()
        {
            logger.Log("Shutting down ThreadManager");
            threadPool.Shutdown(true, 2000);
            threadPool.Dispose();
        }

        #endregion Constructors

        public IDictionary<Guid, IWorkItemResult<object>> GetThreadResults()
        {
            return threadResults;
        }

        public SmartThreadPool GetThreadPool()
        {
            return threadPool;
        }

        #region Methods

        public Guid CreateImageUpdateThread(ImageEntity ImageEnt)
        {
            logger.Log("Entering ThereadManager.CreateImageUpdateThread(ImageEntity)");
            Guid threadID = Guid.NewGuid();
            IWorkItemResult<object> iir =
                threadPool.QueueWorkItem(new Amib.Threading.Func<ImageEntity, object>(DoImageUpdate), ImageEnt);
            threadResults.Add(threadID, iir);
            logger.Log("Exiting ThereadManager.CreateImageUpdateThread(ImageEntity)");
            return threadID;
        }

        public Guid CreateMaintenanceThread(object provider)
        {
            logger.Log("Entering ThreadManager.CreateUpdateThread()");
            Guid threadID = Guid.NewGuid();

            IWorkItemResult<object> wir = null;
            wir =
                threadPool.QueueWorkItem(new Amib.Threading.Func<object, object>(RunMaintenance), provider);
            threadResults.Add(threadID, wir);
            return threadID;
        }

        public Guid CreateUpdateThread(PluginBase provider, object ID)
        {
            logger.Log("Entering ThreadManager.CreateUpdateThread()");
            Guid threadID = Guid.NewGuid();
            ThreadParameters tp = null;
            provider.EnableImageDownloads(true);
            if (ID.GetType() == typeof(MediaClass))
            {
                tp = new ThreadParameters(provider, null, threadID, (MediaClass)ID);
            }
            else
            {
                tp = new ThreadParameters(provider, ID, threadID, null);
            }

            IWorkItemResult<object> wir =
                threadPool.QueueWorkItem(new Amib.Threading.Func<ThreadParameters, object>(DoMediaUpdate), tp);
            threadResults.Add(threadID, wir);

            logger.Log("Exiting ThreadManager.CreateUpdateThread(), rc=" + threadID.ToString());
            return threadID;
        }

        public object GetResult(Guid WorkItem, out bool Complete, out bool Canceled, out Exception exception)
        {
            if (!threadResults.ContainsKey(WorkItem))
            {
                logger.Log(Logger.Levels.ERROR, "WorkItem doesnt exist in results table");
                Complete = false;
                Canceled = false;
                exception = new ArgumentException();
                return null;
            }

            if (threadResults[WorkItem].IsCompleted)
            {
                Complete = true;
                Canceled = false;
                try
                {
                    object result = threadResults[WorkItem].GetResult();
                    exception = null;
                    return result;
                }
                catch (WorkItemResultException e)
                {
                    exception = e;
                    logger.Log(Logger.Levels.ERROR, String.Format("Exception thrown: {0}", e.InnerException));
                    return null;
                }
            }

            if (threadResults[WorkItem].IsCanceled)
            {
                Complete = false;
                Canceled = true;
                exception = null;
                return null;
            }

            Complete = false;
            Canceled = false;
            exception = null;
            return null;
        }

        public void DeleteResults(Guid WorkItem)
        {
            if (threadResults.ContainsKey(WorkItem))
            {
                if (!threadResults[WorkItem].IsCompleted)
                {
                    logger.Log(Logger.Levels.WARN, String.Format("Killing thread for work item: {0} and removing results", WorkItem.ToString()));
                    threadResults[WorkItem].Cancel(true);
                    threadResults.Remove(WorkItem);
                }
                else
                {
                    threadResults.Remove(WorkItem);
                }
            }
            logger.Log(Logger.Levels.WARN, String.Format("WorkItem: {0} does not exist", WorkItem.ToString()));
        }

        public void WaitForResults(List<Guid> WorkItems)
        {
            var ActiveWorkItems = new List<Guid>();
            foreach (Guid guid in WorkItems)
            {
                ActiveWorkItems.Add(guid);
            }
            while (ActiveWorkItems.Count > 0)
            {
                foreach (Guid guid in WorkItems)
                {
                    if (threadResults.ContainsKey(guid))
                    {
                        if (threadResults[guid].IsCompleted || threadResults[guid].IsCanceled)
                        {
                            if (ActiveWorkItems.Contains(guid))
                            {
                                ActiveWorkItems.Remove(guid);
                                logger.Log("Job: " + guid + " complete");
                            }
                        }
                    }
                    else
                    {
                        if (ActiveWorkItems.Contains(guid))
                        {
                            ActiveWorkItems.Remove(guid);
                            logger.Log(Logger.Levels.ERROR, "Job: " + guid + " had no entry in the workitems queue");
                        }
                    }
                }
                logger.Log(Logger.Levels.INFO, String.Format("Waiting on {0} jobs, in use threads: {1}", ActiveWorkItems.Count, threadPool.InUseThreads));
                Thread.Sleep(500);
            }
        }

        private object DoMediaUpdate(ThreadParameters threadParams)
        {
            MediaClass ReturnValue = null;
            try
            {
                if (threadParams.MC != null)
                {
                    ReturnValue = threadParams.PlugIn.GetMedia(threadParams.MC, config.MergeType);
                }
                else
                {
                    ReturnValue = threadParams.PlugIn.GetMediaByID(threadParams.ID);
                }
            }
            catch (Exception e)
            {
                logger.Log(Logger.Levels.ERROR, "Exception thrown by provider: " + e.Message);
                return null;
            }

            return ReturnValue;
        }

        /// <summary>
        ///     Gets the image and updates the DB
        ///     1. If the image download is successful, we update the DB record with the checksum, and thumbnail
        ///     2. If it is not, we delete the record
        /// </summary>
        /// <param name="imageParameters"> </param>
        private object DoImageUpdate(ImageEntity imageEntity)
        {
            logger.Log("Entering thread ThreadManager::DoImageUpdate()");

            Image image = null;
            try
            {
                image = ImageUtils.GetImage(imageEntity.URL);
            }
            catch (Exception e)
            {
                logger.Log(Logger.Levels.ERROR, String.Format("Exception downloading image: {0}", e.Message));
                logger.Log("Deleting ImageEntity from database");
                imageEntity.Delete();
                logger.Log("Exiting thread ThreadManager::DoImageUpdate()");
                return null;
            }

            if (image == null)
            {
                logger.Log(Logger.Levels.ERROR, "Image did not download successfully");
                logger.Log("Deleting ImageEntity from database");
                imageEntity.Delete();
                logger.Log("Exiting thread ThreadManager::DoImageUpdate()");
                return null;
            }

            string tmpImage = Path.GetRandomFileName();
            try
            {
                image.Save(tmpImage);
                image.Dispose();
            }
            catch (Exception e)
            {
                logger.Log(Logger.Levels.ERROR, "Error saving image: " + e.Message);
                File.Delete(tmpImage);
                return null;
            }
            if (!imageEntity.SaveImage(tmpImage, config))
            {
                logger.Log(Logger.Levels.ERROR, "Image save failed, deleting ImageEnttity from database");
                imageEntity.Delete();
                logger.Log("Exiting thread ThreadManager::DoImageUpdate()");
                File.Delete(tmpImage);
                return null;
            }
            File.Delete(tmpImage);
            logger.Log("Successfully updated image");
            logger.Log("Exiting thread ThreadManager::DoImageUpdate()");
            return null;
        }

        private object RunMaintenance(object provider)
        {
            var ProviderShell = (PluginBase)provider;
            try
            {
                ProviderShell.Run();
            }
            catch (Exception e)
            {
                logger.Log(Logger.Levels.ERROR, "Exception running maintenance plugin: " + e.Message);
            }
            return null;
        }

        #endregion Methods
    }
}